feat(experimental): add WAN ReFL as a self-contained training package - #210
Conversation
|
The tiled VAE decode path hardcodes the output buffer batch dimension to 1, while hidden_states may have B > 1. The decoded tile therefore has batch size B and cannot be accumulated in-place into values when B > 1. Since decode() now defaults to tiled=True, this can break existing WAN recipes such as wan21_t2v.yaml, which uses forward_batch_size: 4. Please allocate these buffers using the actual input batch size, apply the same fix to tiled_parallel_decode, and add a regression test covering batch size greater than one. |
|
I support moving WAN ReFL algorithm code into Currently there is only one I suggest removing |
- Remove trainer-side config validation and simplify mean calculation with np.mean. - Remove unused fit=train handling from trainer. - Rename REFL recipe namespace from refl_wan to refl. - Rename base_role.py to role.py for the REFL role implementation. - Move max_grad_norm handling out of the base role and let reward backends own it. - Add a generic reward role under unirl/reward that directly extends the reward service. - Remove reward package pip installation from pyproject configuration. - Remove hardcoded local paths from recipe configs and shell scripts. - Add requirements.txt files for face and videoalign reward backends.
… the wan refl recipe.
- ReflActorRole inherits Remote directly: inline initialize (build bundle/pipeline/backend) + step/save_checkpoint/load_checkpoint, and move RoleStepResult here; delete recipes/common/roles.py - reward role now uses unirl.reward.service.RewardService directly; Trainer.create_remote_role special-cases it so the worker builds the backend, other roles keep the cfg-driven path - RewardService.score_differentiable takes (media_tensor, prompts, records) and forwards records to the backend; update the recipe trainer and the SD3 refl callers to the new signature - move Trainer base from unirl/trainer/trainer.py to recipes/common/trainer.py - configs: reward _target_ -> RewardService; fix the placement comment (cross-process autograd is supported via GradContext RPC)
de650ea to
ec87340
Compare
ec87340 to
e3c6b94
Compare
# Conflicts: # unirl/reward/service.py
…oles DSL - Rebase onto current main (Tencent-Hunyuan#214): RolloutReq/RolloutInputs are gone; the actor role now consumes Texts/Images primitives plus per-sample metadata records straight from the data-source Sample. - Drop recipes/common/ (role-list orchestration): REFLTrainer subclasses BaseTrainer directly and wires actor + reward with placement()+remote_hydra, mirroring RewardBackpropTrainer (the SD3 image-ReFL driver). ReflActorRole mirrors ReFLPolicy's family-agnostic contract (pipeline_target + model_config + from_config; FSDPBackend composed in initialize()). - Re-root both configs from the roles: list to the repo-wide flat schema (actor:/reward:/data_source:/sampling:/logging:), the same shape as examples/diffusion/refl_sd3.yaml. - KL correctness across DP shards: diffuse_with_grad now returns per-sample [B] KL (concat field) instead of a per-shard scalar shared field, so DP_SCATTER merge/re-shard round-trips each shard's own KL. Previously the driver collapsed all shards to one value (loss/logging skew; gradient flow was unaffected because dKL/dkl is the constant kl_weight). - I2V condition assembly moves out of the role into Wan21/Wan22ReflPipeline.build_refl_conditions (mirrors each mainline pipeline's generate); negative prompts ride sampler_kwargs. - Seed scheme now matches ReFLPolicy (base + 1000*rollout_id + dp_rank); previously every rollout redrew the same init noise. - reward/service.py: restore the List typing import lost in the merge.
Regression verification for the P1 review finding: the original per-shard scalar shared_field KL collapsed to rank 0 on DP collect and only lined up on the batch_size == actor_dp == 8 topology. kl_loss is now a per-sample [B] concat column; scripts/verify_refl_kl_batching.py pins chunk/cat round-trips, rewards/KL payload lockstep, per-shard backward grad shapes, unequal actor/reward dp re-chunking, and numeric equivalence with the legacy scalar mean at the pytree wire layer (CPU, no Ray). Shipped as a standalone runnable script rather than a tests/ tree per the Tencent-Hunyuan#99/Tencent-Hunyuan#267 no-unenforced-test-suite policy.
…dtype assert fp32 LoRA params over the bf16 base fail torch 2.7 FSDP2's uniform-original-dtype assertion at first forward (documented in examples/diffusion/refl_sd3.yaml). Omit master_dtype so LoRA stays bf16, matching the SD3 refl recipe's portable choice. Caught by the 8xH20 fleet smoke; the contributor's environment tolerated the fp32 mix.
…rs 5.6 stack Validated on 8xH20 in an isolated transformers==5.6.2 + peft==0.20.0 venv (real VideoReward checkpoint load + differentiable forward/backward: rewards finite, grad_abs_mean=2.2e-3): - vision features: prefer pooler_output — on 5.6 it holds the merged features that fill media placeholders; last_hidden_state is the PRE-merger states (4x tokens, vision dim) and silently corrupts the reward if scattered. Raw-Tensor branch kept for the 4.56 fleet image (TODO drop when the image moves to the locked stack); all other fallbacks removed per the pin-one-version policy. - checkpoint loader: apply the old→new Qwen2VL layout remap in the LoRA-split branch too (module paths are embedded in LoRA keys); guard against double-prefixing already-new-layout keys. - factory: stop forwarding use_cache through from_pretrained — 5.x passes unknown kwargs to the model ctor (TypeError); set it on the config after load, as the author's own disabled-shim note prescribed. - slim stale multi-era compat comments down to the load-bearing facts.
|
transformers 5.6 validation round at
Gate evidence (isolated venv, GPU): VideoReward load → differentiable forward → backward — |
…ed transformers 5.6 only Per review direction: no version-compat branches; a wrong environment fails loudly and the user aligns the env, not the code. - _as_tensor: pooler_output only (drop the 4.56 raw-Tensor concession) - visual property: single 5.6 location (self.model.visual) - checkpoint remap: always applied (target layout is 5.6 by policy), idempotent per key - processor: rely on 5.6 fast-by-default; drop the force-swap + use_fast - attention: hardcode sdpa — flash-attn is not part of the locked stack - pyproject peft floor 0.14 → 0.20: older peft imports transformers cache symbols removed in 5.x and fails at import against the 5.6 pin Gate on 8xH20 (isolated transformers==5.6.2 + peft==0.20.0 venv, real VideoReward ckpt): load + differentiable forward/backward PASS, rewards=[-3.5938, -3.375], grad_abs_mean=3.5e-3.
…d README Review decisions: - recipes/refl/models/ → recipes/refl/model_adaptor/ — the directory holds algorithm-side adaptations wrapping core models to the recipe's BPTT contract, not model definitions; the old name collided with unirl/models semantics and blurred the graduation boundary. - Launch .sh wrappers deleted: zero logic beyond env-var placeholders; the launch surface is one documented command. recipes/refl/README.md now carries the launch examples, layout map, environment policy, and the verification table. Static at this head: compose x2 rc=0; 2270 _target_ paths resolve; KL topology checks pass; full pre-commit green.
…ental/ Core is not just components — the official training flows live in core (train_*.py + the trainers). A layer named 'recipes' reads as the official, recommended usage and pulls that attention to the non-official tier; 'recipe' also already means the flat YAML in this repo's vocabulary (examples/ is the former recipes/ tree). experimental/ names what this tier actually is: the incubation side of a two-way flow — packages start here, mainstream ones get absorbed and solidified into core, ill-fitting core paths move down or out. Internal/private packages live here uncommitted. All dotpaths, the target checker, and docs updated; upstream provenance references (mmrl/recipes/...) intentionally untouched.
…mples/, reward/ Naming rule for the experimental tier (two-way flow): directories holding the SAME kind of content as a core location take the core name, so promotion/demotion is a structural no-op — refl/configs/ → refl/examples/ (mirrors the top-level examples/; graduates into examples/refl/) and refl/rewards/ → refl/reward/ (mirrors unirl/reward/; graduates into unirl/reward/local/). Different-kind content keeps a distinct name on purpose (model_adaptor/ wraps core models to the BPTT contract — it is not a models/ and graduates by merging, not by moving).
…r/ → models/ The mirror rule settles on one criterion: a directory is named after the core home its content graduates into. wan21.py / wan22.py define pipeline and stage classes — the same kind of artifact unirl/models/<m>/ holds — and graduate by merging into those model packages, so the directory mirrors unirl/models/ (plural, matching core exactly; reward/ is singular because unirl/reward/ is). The earlier model_adaptor name predates the mirror rule; the fork-confusion concern it addressed is now handled by the graduation notes in the layout table and module docstrings.
…nc main Mechanics PASS on the declared 5.6 stack (9.5h, 228s/step, clean exit, no reward collapse, grads healthy). Reward is FLAT at this horizon on substitute assets (pickscore prompts + VideoReward-2B): segment means 1.91/1.95/1.91/2.07/1.80/1.95, first25→last25 +2.4%, OLS slope ~0. Learning-effect verdict deferred to longer horizons / original assets; lr-sensitivity diagnostic tracked in PR Tencent-Hunyuan#210.
… table The 150-rollout run was a maintainer-side sanity diagnostic on substitute assets; its record lives in the PR Tencent-Hunyuan#210 discussion, not in the package's verification table, which lists only runs with product-level standing.
…mantics Checking pass on the flat trend run found this adjustment-era divergence: the role was rewriting params.seed per rollout/rank (decorrelated noise), while the contributor's verified 835-rollout curve trains DRaFT on a fixed initial noise (seed used verbatim, eta=0 ODE). Ship his regime; varying noise is a semantics change that needs its own evidence. Drops the now consumer-less rollout_id plumbing from generate_samples/train_step.
…es lora_A Checking follow-up on the flat trend run. The contributor's configs carried master_dtype: fp32; the adjustment dropped it to dodge FSDP2's uniform-original-dtype assert on the fleet image's torch 2.7.1. That assert is an environment-misalignment symptom, not a code constraint: the pinned torch family (2.11+, via the sglang extra) checks dtype uniformity over trainable params only, and the FSDP backend's master_dtype path implements exactly this bf16-base + fp32-LoRA-master regime. Checkpoint forensics on the 150-rollout run show the bf16 master froze lora_A entirely (delta ~0.0001% over 100 steps; AdamW step 5e-6 < bf16 ULP at |A|~0.1) while the contributor's fp32 runs train the full (A, B). Align the environment, not the config.
Same correction as the wan configs on #210: the bf16-master variant was an environment workaround for the fleet image's torch 2.7.1 (FSDP2 uniform-dtype assert over all params); the pinned torch checks trainables only, and the historical SD3 ReFL curves were produced with the fp32 master.
…vention Writes down the experimental-tier rules converged in the #210 review and makes the mechanical ones lint-enforced: - experimental/README.md — the contract: two-way flow with core (official flows live in core; mainstream packages graduate up, ill-fitting core paths move down), package anatomy with mirror naming (models/ reward/ examples/ named after the core home content graduates into), locked single-stack policy, additive-only requirements with the differentiable⇒same-process⇒same-stack corollary, owner+verification table requirement. - scripts/check_experimental_boundaries.py (pre-commit): core never imports experimental; packages never import each other; recipe requirements may not re-declare core dependencies (same-process colocation cannot version-isolate). - experimental/private_*/ gitignored; the tier is not packaged into the wheel, so private code cannot leak into a build.
The index gate hardcoded PACKAGE = "unirl" from before main broadened _TARGET_RE to unirl|experimental (#210), so experimental.* targets were extracted but could never resolve. Derive the regex from PACKAGES so the accepted roots and the index can't drift apart again.
scripts/ kept accumulating non-guard files because its name promised generic tooling space (#158's ep_verify/, #210's verify script). Name the folder after its real contract instead: lint/ holds exactly the scripts wired into .pre-commit-config.yaml, and CLAUDE.md now states the positive rule (verification harness results are quoted in the PR Test Plan, not committed).
feat(experimental): add WAN ReFL as a self-contained training package
Summary
This PR moves algorithm-specific training flows out of UniRL core and organizes them as self-contained recipes, landing
experimental/refl— WAN ReFL/BPTT (direct differentiable-reward backprop) for Wan 2.1 T2V and Wan 2.2 I2V — as the first recipe. The recipe is built purely on existing core seams (placement()+remote_hydra+_target_late binding); it introduces no new orchestration layer.experimental/reflpackage (trainer + roles + models + reward + examples + README, self-contained):REFLTrainersubclassesunirl.trainer.base.BaseTrainerdirectly and wires two colocated roles (actor + differentiable reward), mirroringRewardBackpropTrainer(the SD3 image-ReFL driver).ReflActorRolemirrorsReFLPolicy's family-agnostic contract:pipeline_target+model_config→Pipeline.from_config, withFSDPBackendcomposed ininitialize(). Payloads are sample-native primitives (Texts/Images+ per-sample metadata records) — no request objects.Wan21ReflPipeline/Wan22ReflPipelinesubclass the mainline pipelines and swap in adiffuse_with_gradBPTT stage (truncatedmid_timestep..final_timestepgrad window, single-branch CFG, optional per-step KL against the LoRA-disabled reference). The BPTT stage contract (DiffuseWithGradResult) is recipe-local by design: concrete stages inherit theDiffusionStageProtocol explicitly, so a protocol-level stub would become a realNone-returning method on every stage and defeathasattrcapability checks; the promotion path is a separate opt-in protocol (theDifferentiableRewardidiom) once a second out-of-recipe BPTT consumer exists. Mainline WAN diffusion/pipeline code is untouched.experimental/refl/reward/, each with its ownrequirements.txt(core env stays clean).examples/):experimental/refl/examples/{wan21_t2v_videoalign_refl,wan22_i2v_face_refl}.yaml.Core enablers (framework-level, shared with future BPTT recipes):
Worker.call;RewardService.score_differentiable(media_tensor, prompts, records)widened to video tensors + per-sample metadata;DifferentiableRewardprotocol updated to match.WanVideoVAE(nested grad checkpoint + activation-grad-only conv) deliberately replaces the WAN decode implementation in core — memory/numeric VAE optimizations are model assets shared by GRPO and ReFL, not algorithm property; forking the decode contract per algorithm was rejected in review. Hub repo-id checkpoints (the mainlinewan21_t2v.yamldefault) resolve through the HF cache (snapshot_download), preserving the previousfrom_pretrainedloading semantics.LoraConfig.module_prefix(Wan 2.2 trains only thelow_noiseDiT),linear_warmupLR schedule,MultimodalRLDataSourcerun.shuffle.Review adjustments (2026-07-30, maintainer push at
ec561c7a)Fast-forward on top of the author's head
e3c6b940(nothing rewritten), applying the review consensus and re-aligning with main:RolloutReq/RolloutInputsafter this PR's last merge; the recipe now consumesSampleprimitives end-to-end.recipes/common/, −422 lines) per review:REFLTrainer(BaseTrainer)+remote_hydrareplace the role-list orchestration; configs re-rooted fromroles:to the repo-wide flat schema. Net −322 lines vs the previous head while keeping all functionality.kl_losswas a per-shard scalarshared_field— DP collect kept only rank 0's KL and re-broadcast it, which only lined up on the verifiedbatch == actor_dp == 8topology (logs duplicated rank 0; other B/dp splits risked shape mismatch). Now a per-sample[B]concat column;scripts/verify_refl_kl_batching.pypins the wire-layer invariants across B==dp / B>dp / non-power-of-two / dp==1 / unequal actor-reward dp (standalone script per the test: remove tests directory #99/test: remove tests directory #267 no-unenforced-test-tree policy).params.seedused verbatim, every rollout/rank — the regime your 835-rollout curve was trained in). An earlier adjustment decorrelated noise per rollout/rank; a checking pass flagged that as an unevidenced semantics change and reverted it (fe2788c6).Wan2xReflPipeline.build_refl_conditions(mirrors each mainlinegenerate); the role has no per-model imports.Expected behavior deltas vs the curves below: per-shard-correct KL values (the Wan 2.2
kl_weight=1.0loss/KL curves may shift slightly — that is the correction) and the bf16 LoRA master (fp32 master trips a torch 2.7 FSDP2 uniform-dtype assert; a dedicated fp32 LoRA param-group fix is planned as a follow-up). Curve shapes are otherwise expected to match.Second pass (
e0497399), closing the remaining review findings:requirements.txtcan only add packages — it can never version-"isolate". The old file pinnedtransformers==4.45.2/peft==0.10.0/flash-attn==2.5.8, which would downgrade the core stack (transformers>=5.6,peft>=0.14) on install. VideoAlign now runs on the core stack: the wrapper's documented 5.x shim is enabled (explicitmm_token_type_idsblack-list pop — never signature filtering, which PEFT breaks), flash-attn falls back to SDPA when absent, andrequirements.txtis additive-only.WanVideoVAE.load_from_diffusersreads local files only;WAN21Bundlenow resolves non-local paths through the HF cache first, so mainline repo-id configs keep loading.DiffuseWithGradResult+ thediffuse_with_gradcontract moved out of the coreDiffusionStageProtocol (explicitly-inherited protocol stubs madehasattrcapability checks vacuously true on every stage) intoexperimental/refl/models/types.py.actor.kl_weightowns on/off + weight and is injected into the stage'ssampler_kwargs; a stalesampling.sampler_kwargs.kl_weightraises instead of being silently overridden.0 <= mid <= final < T) at diffuse time; launch scripts run underset -euo pipefail; the Face reference-embedding cache is a bounded LRU (64).experimental/— core carries the official training flows, and a layer namedrecipesread as the official usage (the word also already means the flat YAML here).experimental/names the incubation side of the intended two-way flow: mainstream packages get absorbed and solidified into core; internal/private packages live here uncommitted. Inside a package, directories are named after the core home their content graduates into (structural no-op promotion):examples/mirrors the top-levelexamples/,reward/mirrorsunirl/reward/,models/mirrorsunirl/models/(the BPTT pipeline/stage subclasses graduate by merging into the matching model packages). Launch.shwrappers were deleted — the launch surface is one documented command, now inexperimental/refl/README.mdtogether with the layout map, environment policy, and the verification table.Test Plan
export DATA_PATH=/path/to/prompts.txt PRETRAINED_MODEL=/path/to/Wan2.1-T2V-1.3B-Diffusers \ VIDEOALIGN_MODEL_PATH=/path/to/VideoReward RAY_ADDRESS=auto python -m experimental.refl.run --config-name=wan21_t2v_videoalign_refl num_devices=8pip install -r experimental/refl/reward/face/requirements.txt export PRETRAINED_MODEL=/path/to/Wan2.2-I2V-A14B-Diffusers DATA_PATH=/path/to/i2v_prompts.jsonl \ FACE_MODEL_PATH=/path/to/antelodev2 RAY_ADDRESS=auto python -m experimental.refl.run --config-name=wan22_i2v_face_refl num_devices=8Static validation at
e0497399(CPU, 2026-07-30):python -m experimental.refl.run --config-name=... --cfg job --resolve) — rc=0 / rc=0;scripts/check_recipe_targets.py— 2270_target_paths resolve (incl.recipes.*);ReflActorRolector for both configs;python scripts/verify_refl_kl_batching.py— all topology checks pass;pre-commit run --all-filespasses;unirl/imports nothing fromexperimental/.Maintainer GPU smoke (2026-07-30, head
40b3f4c9, 8xH20 fleet imageunirl-v1d: torch 2.7.1 / transformers 4.56.0.dev0 / peft 0.17.1):wan21_t2v_videoalign_refl(num_rollouts=2, full 81f/480x832 geometry, local WAN ckpt + VideoReward-2B): PASS, clean exit —rollout 1/2 reward=1.9839 loss=-0.4960 grad_norm=31.1 (230s),rollout 2/2 reward=3.1245 grad_norm=25.3 (228s). Finite nonzero grad_norm = gradients flow reward → VAE → DiT LoRA across the RPC chain.d96800dc,40b3f4c9):REFLTrainernow storescfg(BaseTrainer does not retain it), and the configs dropmaster_dtype: fp32— fp32 LoRA over the bf16 base trips torch 2.7 FSDP2's uniform-original-dtype assert (the documentedrefl_sd3.yamlnote); LoRA master weights are now bf16, matching the SD3 refl recipe.Still pending on GPU (owner: @YSunLIN):
wan22_i2v_face_reflsmoke at the current head (needs the face env extras + your I2V dataset withref_video_pathmetadata);mm_token_type_idspath of the VideoAlign shim (fleet image is 4.56 → the pop is a no-op there; needs a transformers>=4.58 env);wan21_t2vtrainside run (or Hub repo-id load + decode eyeball) covering theWanVideoVAEswap;WanVideoVAEand the VideoAlign port, per the repovendor/VENDOR_COMMIT.txtconvention.Verification Status
wan21_t2v_videoalign_reflnum_rollouts=835;batch_size=8;num_inference_steps=25; final-step differentiable pathwan22_i2v_face_reflnum_rollouts=690;batch_size=8;module_prefix=low_noise;num_inference_steps=8Wan 2.1 T2V ReFL training reward curve
Wan 2.2 I2V ReFL training reward curve
Compatibility / Risk
experimental/reflis a new, self-contained path; existing entrypoints and the mainline WAN GRPO flow are unaffected.experimental/is repo-run only for now (not in wheel packaging) — a follow-up recipes-contract PR will settle packaging + import-direction CI.RewardService.score_differentiablesignature changed (media_tensor, prompts, records); the two core ReFL call sites (unirl/trainer/refl.py) are updated in this PR. A follow-up PR will remove the legacy core ReFL path (train_refl.py/trainer/refl.py/train/refl/) so refl lives only in recipes.WanVideoVAE; dtype/tiling/recompute may differ slightly from the old path numerically — monitored via reward curves and sample quality.MultimodalRLDataSource.run.shuffledefaults totrue(previous behavior preserved);module_prefixis opt-in and backward compatible — confirm trainable params via startup logs.requirements.txtfiles are additive-only by contract (same-process colocation makes version isolation impossible); rewards that genuinely need a conflicting stack belong in the out-of-processunirl-reward-service, which is only an option for non-differentiable rewards.AI-assisted: the 2026-07-30 adjustment was implemented and statically validated with Claude Code under maintainer direction (haonan3); every change reviewed against the review thread consensus. Duplicate-work check: supersedes the orchestration-layer portion only; overlaps were reconciled with #188's direction in the review discussion.